// 文件: db/schema.go
// Package db db/schema.go 包含与数据库模式（表结构、索引等）相关的操作
package db

import (
	"database/sql"                   // 标准数据库接口
	"errors"                         // 用于创建标准错误
	"fmt"                            // 用于格式化字符串和错误
	log "github.com/sirupsen/logrus" // 日志库
	"strings"                        // 用于字符串操作
	"time"
)

// KeyInfo 表示表的键信息 (主键或唯一索引)
// 这个结构体用于内部处理，与 models.KeyInfo 可能略有不同
type KeyInfo struct {
	Name      string   // 索引名 (例如 PRIMARY, uk_user_code)
	Columns   []string // 组成索引的列名 (按顺序)
	IsUnique  bool     // 是否是唯一索引 (包括主键)
	IsPrimary bool     // 是否是主键
}

var (
	// ErrNoValidKeyFound 定义当表缺少主键或唯一键时的错误
	ErrNoValidKeyFound = errors.New("表缺少主键或唯一索引")
	// 定义错误常量，便于统一错误处理和测试

	// ErrNilDBConn 数据库连接为空错误
	ErrNilDBConn = errors.New("数据库连接为空")

	// ErrEmptySchemaOrTable 模式名或表名为空错误
	ErrEmptySchemaOrTable = errors.New("模式名或表名为空")

	// ErrEmptyKeyValueList 键值列表为空错误
	ErrEmptyKeyValueList = errors.New("键值列表为空")

	// ErrKeyValueMismatch 键值数量不匹配错误
	ErrKeyValueMismatch = errors.New("键值数量与键列数量不匹配")
)

// GetTableColumns 获取指定表的所有列名，并按其在表中的顺序返回。
// 参数:
//   - db: 数据库连接 (*sql.DB)
//   - schemaName: 数据库模式名
//   - tableName: 表名
//
// 返回值:
//   - []string: 包含所有列名的字符串切片，按表定义顺序排列。
//   - error: 如果查询失败或表不存在/无列，则返回错误。
func GetTableColumns(db *sql.DB, schemaName, tableName string) ([]string, error) {
	// 参数验证
	if db == nil {
		return nil, errors.New("数据库连接无效 (nil)")
	}
	if schemaName == "" || tableName == "" {
		return nil, errors.New("数据库名和表名不能为空")
	}

	// 构建查询 information_schema.COLUMNS 的 SQL
	// 添加注释方便追踪查询来源
	query := `
        SELECT /* GetTableColumns */ COLUMN_NAME
        FROM information_schema.COLUMNS
        WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
        ORDER BY ORDINAL_POSITION /* 保证按列顺序返回 */
    `
	// 执行查询
	rows, err := db.Query(query, schemaName, tableName)
	if err != nil {
		return nil, fmt.Errorf("查询表 %s.%s 的列信息失败: %w", schemaName, tableName, err)
	}
	// 确保结果集关闭
	defer rows.Close()

	var columns []string // 存储列名
	// 遍历结果行
	for rows.Next() {
		var colName string
		// 扫描列名
		if err := rows.Scan(&colName); err != nil {
			return nil, fmt.Errorf("扫描表 %s.%s 的列名失败: %w", schemaName, tableName, err)
		}
		columns = append(columns, colName) // 添加到结果切片
	}

	// 检查遍历过程中的错误
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("处理表 %s.%s 列信息结果集时出错: %w", schemaName, tableName, err)
	}

	// 如果没有找到任何列，说明表可能不存在或为空
	if len(columns) == 0 {
		return nil, fmt.Errorf("表 %s.%s 不存在或没有列", schemaName, tableName)
	}

	// 返回列名切片
	return columns, nil
}

// GetBestUniqueKey 获取用于数据分块 (chunking) 的最佳唯一键。
// 优先选择主键 (PRIMARY KEY)，如果不存在主键，则选择按名称排序的第一个唯一索引 (UNIQUE INDEX)。
//
// 参数:
//   - db: 数据库连接 (*sql.DB)
//   - schemaName: 数据库模式名
//   - tableName: 表名
//
// 返回值:
//   - *KeyInfo: 包含最佳键信息的结构体指针。
//   - error: 如果查询失败或找不到任何主键或唯一索引，则返回错误。
func GetBestUniqueKey(db *sql.DB, schemaName, tableName string) (*KeyInfo, error) {
	// 参数验证
	if db == nil {
		return nil, errors.New("数据库连接无效 (nil)")
	}
	if schemaName == "" || tableName == "" {
		return nil, errors.New("数据库名和表名不能为空")
	}

	// 构建查询 information_schema.STATISTICS 的 SQL
	// 只选择 NON_UNIQUE = 0 的索引（即主键和唯一索引）
	// 按 INDEX_NAME 和 SEQ_IN_INDEX 排序，确保同一索引的列按其在索引中的顺序处理
	query := `
        SELECT /* GetBestUniqueKey */
            INDEX_NAME,
            COLUMN_NAME,
            NON_UNIQUE,
            SEQ_IN_INDEX
        FROM information_schema.STATISTICS
        WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND NON_UNIQUE = 0
        ORDER BY INDEX_NAME, SEQ_IN_INDEX
    `

	// 执行查询
	rows, err := db.Query(query, schemaName, tableName)
	if err != nil {
		return nil, fmt.Errorf("查询表 %s.%s 的唯一索引信息失败: %w", schemaName, tableName, err)
	}
	defer rows.Close()

	// 使用 map 存储每个索引的信息，键为索引名
	keys := make(map[string]*KeyInfo)
	// 使用切片记录索引的发现顺序，因为 map 迭代顺序不固定
	var keyOrder []string

	log.Debugf("开始查找表 %s.%s 的唯一键/主键...", schemaName, tableName)

	// 遍历查询结果
	for rows.Next() {
		var indexName, columnName string
		var nonUnique int  // 理论上总是 0
		var seqInIndex int // 列在索引中的顺序 (1-based)

		// 扫描当前行数据
		if err := rows.Scan(&indexName, &columnName, &nonUnique, &seqInIndex); err != nil {
			return nil, fmt.Errorf("扫描表 %s.%s 的索引信息失败: %w", schemaName, tableName, err)
		}

		// 记录从 information_schema 读取到的原始信息 (用于调试)
		log.Debugf("  读取到索引信息: IndexName=%s, ColumnName=%s, NonUnique=%d, SeqInIndex=%d",
			indexName, columnName, nonUnique, seqInIndex)

		// 检查该索引是否已记录
		key, exists := keys[indexName]
		if !exists {
			// 如果是第一次遇到该索引名，创建新的 KeyInfo 结构体
			key = &KeyInfo{
				Name:      indexName,
				Columns:   make([]string, 0),                       // 初始化列名切片
				IsUnique:  true,                                    // 因为 WHERE 条件是 NON_UNIQUE = 0
				IsPrimary: strings.ToUpper(indexName) == "PRIMARY", // 判断是否为主键
			}
			keys[indexName] = key                  // 存入 map
			keyOrder = append(keyOrder, indexName) // 记录发现顺序
			log.Debugf("  发现新索引: %s (Primary: %t)", indexName, key.IsPrimary)
		}

		// 将列名按 SEQ_IN_INDEX 添加到 KeyInfo.Columns 切片的正确位置
		targetIndex := seqInIndex - 1 // 将 1-based 转换为 0-based 索引
		if targetIndex < 0 {
			// SEQ_IN_INDEX 不应该小于 1
			log.Errorf("错误: 表 %s.%s 索引 %s 列 %s 的 SEQ_IN_INDEX 值 %d 无效。",
				schemaName, tableName, indexName, columnName, seqInIndex)
			return nil, fmt.Errorf("索引 %s 的 SEQ_IN_INDEX 无效: %d", indexName, seqInIndex)
		}

		// 动态扩展 Columns 切片以容纳当前列
		// 如果 targetIndex 超出当前切片长度，需要扩展
		if targetIndex >= len(key.Columns) {
			neededSize := targetIndex + 1
			// 创建一个足够大的新切片
			newColumns := make([]string, neededSize, neededSize+2) // 预留一些额外容量
			// 复制旧切片内容到新切片
			copy(newColumns, key.Columns)
			key.Columns = newColumns // 更新 KeyInfo 中的切片引用
			log.Debugf("  为索引 %s 扩展列切片到长度 %d 以容纳 SeqInIndex %d", indexName, neededSize, seqInIndex)
		}

		// 检查目标位置是否已被占用（正常情况下不应发生）
		if key.Columns[targetIndex] != "" {
			log.Warnf("警告: 表 %s.%s 索引 %s 的位置 %d 已被列 '%s' 占用，现被 '%s' 覆盖。检查 information_schema.STATISTICS 数据。",
				schemaName, tableName, indexName, targetIndex, key.Columns[targetIndex], columnName)
		}

		// 将列名放入正确的位置
		key.Columns[targetIndex] = columnName
		log.Debugf("  将列 '%s' 添加到索引 '%s' 的位置 %d", columnName, indexName, targetIndex)
	} // 结束 rows.Next()

	// 检查遍历过程中的错误
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("处理表 %s.%s 索引信息结果集时出错: %w", schemaName, tableName, err)
	}

	// 检查是否找到了任何唯一键/主键
	if len(keys) == 0 {
		log.Errorf("错误: 表 %s.%s %v，无法进行基于键的校验。", schemaName, tableName, ErrNoValidKeyFound)
		return nil, fmt.Errorf("%w: 表 %s.%s", ErrNoValidKeyFound, schemaName, tableName)
	}

	// 记录找到的所有唯一键/主键信息 (用于调试)
	log.Debugf("表 %s.%s 找到的唯一键/主键列表:", schemaName, tableName)
	for _, idxName := range keyOrder {
		keyInfo := keys[idxName]
		log.Debugf("  - 索引名: %s, 是否主键: %t, 列: %v (共 %d 列)", keyInfo.Name, keyInfo.IsPrimary, keyInfo.Columns, len(keyInfo.Columns))
		// 检查是否有空列名（可能表示 SEQ_IN_INDEX 不连续）
		for i, col := range keyInfo.Columns {
			if col == "" {
				log.Warnf("  ! 警告: 索引 %s 的位置 %d 处列名为空！", idxName, i)
			}
		}
	}

	// **选择最佳键的逻辑:**
	// 1. 优先选择主键 (PRIMARY)
	if primaryKey, exists := keys["PRIMARY"]; exists {
		// 检查主键的列是否都有效（没有空字符串）
		validPK := true
		for i, col := range primaryKey.Columns {
			if col == "" {
				log.Errorf("错误: 表 %s.%s 的主键 (PRIMARY) 在位置 %d 处列名为空，无法使用此键。", schemaName, tableName, i)
				validPK = false
				break
			}
		}
		if validPK {
			log.Infof("表 %s.%s: 使用主键 %v (共 %d 列) 作为校验键", schemaName, tableName, primaryKey.Columns, len(primaryKey.Columns))
			return primaryKey, nil // 返回主键
		} else {
			log.Warnf("表 %s.%s: 主键存在但列信息不完整，尝试查找其他唯一索引。", schemaName, tableName)
			// 继续尝试查找其他唯一索引
		}
	}

	// 2. 如果没有有效主键，则按发现顺序查找第一个有效的唯一索引
	for _, indexName := range keyOrder {
		if indexName == "PRIMARY" {
			continue // 跳过主键（已处理或无效）
		}
		uniqueKey := keys[indexName]
		// 检查该唯一索引的列是否都有效
		validUK := true
		for i, col := range uniqueKey.Columns {
			if col == "" {
				log.Warnf("警告: 表 %s.%s 的唯一索引 %s 在位置 %d 处列名为空，跳过此索引。", schemaName, tableName, indexName, i)
				validUK = false
				break
			}
		}
		if validUK {
			log.Infof("表 %s.%s: 未使用主键，改用唯一索引 '%s' (%v) (共 %d 列) 作为校验键", schemaName, tableName, uniqueKey.Name, uniqueKey.Columns, len(uniqueKey.Columns))
			return uniqueKey, nil // 返回第一个找到的有效唯一索引
		}
	}

	// 如果遍历完所有索引都没有找到合适的键
	log.Errorf("错误: 未能为表 %s.%s 确定有效的校验键（主键或唯一索引）。", schemaName, tableName)
	return nil, fmt.Errorf("%w: 表 %s.%s (所有键均无效或不完整)", ErrNoValidKeyFound, schemaName, tableName)
}

// PrepareChecksumTable 检查并（如果不存在）创建用于存储校验结果的 t_checksum 表。
// 参数:
//   - db: 校验数据库的连接 (*sql.DB)
//
// 返回值:
//   - error: 如果创建或检查表失败，则返回错误。
func PrepareChecksumTable(db *sql.DB) error {
	// 参数验证
	if db == nil {
		return errors.New("数据库连接无效 (nil)")
	}

	// 定义 t_checksum 表的 SQL 创建语句
	// 使用 IF NOT EXISTS 避免表已存在时出错
	// 为字段添加注释，说明其用途
	// 优化索引：PRIMARY KEY 覆盖了 (dbid, schema, table, chunk, time) 的查询，
	//            添加 (schema, table) 的索引用于按表查询。
	//            添加 (dbid, schema, table) 的索引优化按DBID查找表的场景。
	query := `
        CREATE TABLE IF NOT EXISTS t_checksum (
          f_dbid VARCHAR(100) NOT NULL COMMENT '数据库标识 (例如 host:port 或自定义 ID)',
          f_schema_name VARCHAR(64) NOT NULL COMMENT '库名',
          f_table_name VARCHAR(64) NOT NULL COMMENT '表名',
          f_chunk_no INT UNSIGNED NOT NULL COMMENT 'Chunk 编号 (从1开始)',
          f_create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间戳',
          f_min_key VARCHAR(512) NOT NULL COMMENT 'Chunk 起始键值 (多列用逗号连接)',
          f_max_key VARCHAR(512) NOT NULL COMMENT 'Chunk 结束键值 (多列用逗号连接)',
          /* 使用 BIGINT UNSIGNED 存储 CRC32 值，与 MySQL CRC32() 函数结果类型一致 */
          f_chunk_crc32 BIGINT UNSIGNED DEFAULT NULL COMMENT 'Chunk CRC32 校验和',
          f_rows_count INT UNSIGNED DEFAULT NULL COMMENT 'Chunk 内的行数',
          /* 主键包含创建时间，允许同一块重算 */
          PRIMARY KEY (f_dbid, f_schema_name, f_table_name, f_chunk_no, f_create_time),
          /* 索引用于按库表快速查找 */
          INDEX idx_schematable (f_schema_name, f_table_name),
          /* 优化按DBID+库表查询 */
          INDEX idx_dbid_schematable (f_dbid, f_schema_name, f_table_name)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表数据一致性校验结果存储表';
    `
	// 执行 CREATE TABLE IF NOT EXISTS 语句
	_, err := db.Exec(query)
	if err != nil {
		// 如果执行失败，返回包含原始错误的错误信息
		return fmt.Errorf("创建或检查 t_checksum 表失败: %w", err)
	}

	// 记录成功信息
	log.Info("t_checksum 表已准备就绪")
	return nil // 表示成功
}

// BuildChunkSQL 为给定表构建分块查询的SQL模板
//
// 该函数从数据库获取表的结构信息，并构建适用于分块查询的SQL模板。
// 生成的SQL将使用WHERE子句根据主键或唯一索引条件进行过滤。
//
// 参数:
//   - ctx: 上下文，用于超时控制和取消操作
//   - dbConn: 数据库连接，用于获取表结构信息
//   - schemaName: 数据库名
//   - tableName: 表名
//
// 返回值:
//   - string: SQL查询模板，包含?占位符
//   - int: 键列数量
//   - *db.KeyInfo: 表键信息，包含列名和类型
//   - error: 可能的错误
func BuildChunkSQL(dbConn *sql.DB, schemaName, tableName string) (string, int, *KeyInfo, error) {
	if dbConn == nil {
		return "", 0, nil, ErrNilDBConn
	}

	if schemaName == "" || tableName == "" {
		return "", 0, nil, fmt.Errorf("%w: schemaName=%q, tableName=%q",
			ErrEmptySchemaOrTable, schemaName, tableName)
	}

	log.WithFields(log.Fields{
		"schema": schemaName,
		"table":  tableName,
	}).Debug("开始构建分块查询SQL")

	startTime := time.Now()

	// 获取表的最佳唯一键
	keyInfo, err := GetBestUniqueKey(dbConn, schemaName, tableName)
	if err != nil {
		log.WithFields(log.Fields{
			"schema": schemaName,
			"table":  tableName,
			"error":  err,
		}).Error("错误: 表 " + schemaName + "." + tableName + " 没有找到任何主键或唯一索引，无法进行基于键的校验。")

		// 检查错误消息，保持与测试期望一致
		if strings.Contains(err.Error(), "缺少主键或唯一索引") {
			return "", 0, nil, fmt.Errorf("未找到表的主键或唯一索引")
		}
		return "", 0, nil, fmt.Errorf("获取表键信息失败: %w", err)
	}

	if keyInfo == nil || len(keyInfo.Columns) == 0 {
		log.WithFields(log.Fields{
			"schema": schemaName,
			"table":  tableName,
		}).Error("错误: 表 " + schemaName + "." + tableName + " 没有找到任何主键或唯一索引，无法进行基于键的校验。")
		return "", 0, nil, fmt.Errorf("未找到表的主键或唯一索引")
	}

	log.WithFields(log.Fields{
		"schema":     schemaName,
		"table":      tableName,
		"key_name":   keyInfo.Name,
		"key_cols":   keyInfo.Columns,
		"is_primary": keyInfo.IsPrimary,
	}).Debug("找到表的键信息")

	// 获取表的所有列
	columns, err := GetTableColumns(dbConn, schemaName, tableName)
	if err != nil {
		return "", 0, nil, fmt.Errorf("获取表列信息失败: %w", err)
	}

	if len(columns) == 0 {
		return "", 0, nil, fmt.Errorf("表 %s.%s 没有列", schemaName, tableName)
	}

	// 使用strings.Builder提高字符串拼接性能
	var selectBuilder strings.Builder
	selectBuilder.WriteString("SELECT ")

	// 构建SELECT部分的列列表
	// 首先添加键列，然后是非键列
	keyColMap := make(map[string]bool)
	for _, keyCol := range keyInfo.Columns {
		keyColMap[keyCol] = true
		selectBuilder.WriteString(fmt.Sprintf("`%s`, ", keyCol))
	}

	// 添加其他非键列
	nonKeyColCount := 0
	for _, col := range columns {
		if !keyColMap[col] {
			selectBuilder.WriteString(fmt.Sprintf("`%s`, ", col))
			nonKeyColCount++
		}
	}

	// 移除最后的逗号和空格
	selectClause := strings.TrimSuffix(selectBuilder.String(), ", ")

	// 构建WHERE条件
	// 对于多列键，我们需要创建复合条件 (col1,col2,...) > (?,?,...)
	var whereBuilder strings.Builder
	whereBuilder.WriteString(" FROM `")
	whereBuilder.WriteString(schemaName)
	whereBuilder.WriteString("`.`")
	whereBuilder.WriteString(tableName)
	whereBuilder.WriteString("` WHERE ")

	// 构建键列表达式 (col1,col2)
	var keyColBuilder strings.Builder
	keyColBuilder.WriteString("(")
	for i, col := range keyInfo.Columns {
		if i > 0 {
			keyColBuilder.WriteString(",")
		}
		keyColBuilder.WriteString("`")
		keyColBuilder.WriteString(col)
		keyColBuilder.WriteString("`")
	}
	keyColBuilder.WriteString(")")
	keyColExpr := keyColBuilder.String()

	// 构建参数占位符 (?,?)
	placeholders := make([]string, len(keyInfo.Columns))
	for i := range keyInfo.Columns {
		placeholders[i] = "?"
	}
	paramExpr := fmt.Sprintf("(%s)", strings.Join(placeholders, ","))

	// 完成WHERE子句
	whereBuilder.WriteString(keyColExpr)
	whereBuilder.WriteString(" > ")
	whereBuilder.WriteString(paramExpr)

	// 添加ORDER BY子句
	whereBuilder.WriteString(" ORDER BY ")
	for i, col := range keyInfo.Columns {
		if i > 0 {
			whereBuilder.WriteString(", ")
		}
		whereBuilder.WriteString("`")
		whereBuilder.WriteString(col)
		whereBuilder.WriteString("`")
	}

	// 添加LIMIT子句
	whereBuilder.WriteString(" LIMIT 1000")

	// 组合最终SQL
	finalSQL := selectClause + whereBuilder.String()

	log.WithFields(log.Fields{
		"schema":        schemaName,
		"table":         tableName,
		"sql_length":    len(finalSQL),
		"key_columns":   len(keyInfo.Columns),
		"total_columns": len(columns),
		"duration_ms":   time.Since(startTime).Milliseconds(),
	}).Debug("构建分块查询SQL完成")

	return finalSQL, len(keyInfo.Columns), keyInfo, nil
}
